package com.mlming.simpledb.backend.dm;

import com.google.common.primitives.Bytes;
import com.mlming.simpledb.backend.common.SubArray;
import com.mlming.simpledb.backend.dm.dataItem.DataItem;
import com.mlming.simpledb.backend.dm.logger.Logger;
import com.mlming.simpledb.backend.dm.page.Page;
import com.mlming.simpledb.backend.dm.page.PageXDataHandler;
import com.mlming.simpledb.backend.dm.pageCache.PageCache;
import com.mlming.simpledb.backend.tm.TransactionManager;
import com.mlming.simpledb.backend.utils.Panic;
import com.mlming.simpledb.backend.utils.Parser;
import com.sun.scenario.effect.Offset;

import java.util.*;

public class Recover {
    // **data的定义是**:
    // insertLog:
    //      [LogType][XID][Pgno][Offset][Raw]: xid对应的事务 在页号为Pgno的页面的offset位置 插入Raw数据
    // updateLog:
    //      [LogType][XID][Pgno][Offset][OldRaw][NewRaw]:  xid对应的事务 在页号为Pgno的页面的offset位置 把原值oldRaw修改为newRaw
    private static final byte LOG_TYPE_INSERT = 0; // 插入LogType
    private static final byte LOG_TYPE_UPDATE = 1;// 更新LogType

    private static final int REDO = 0; // 表示当前这个操作是redo操作
    private static final int UNDO = 1; // 表示当前这个操作是undo操作

    // 根据data的格式, 定义以下偏移量:
    // LogType: 1 , XID: 8(long), Pgno: 4(int), offset: 2(short) , Raw: 不确定
    private static final int OF_TYPE = 0;
    private static final int OF_XID = OF_TYPE + 1;
    private static final int OF_PGNO = OF_XID + 8;
    private static final int OF_OFFSET = OF_PGNO + 4;
    private static final int OF_RAW = OF_OFFSET + 2;// 如果是update,则对半分开

    // 根据data的格式, 定义了InsertLogInfo类
    static class InsertLogInfo {
        long xid;
        int pgno;
        short offset;
        byte[] raw;
    }

    // 根据data的格式, 定义了UpdateLogInfo类
    static class UpdateLogInfo {
        long xid;
        int pgno;
        short offset;
        byte[] oldRaw;
        byte[] newRaw;
    }

    // 开始崩溃恢复:
    public static void recover(TransactionManager tm, Logger lg, PageCache pc) {
        System.out.println("Recovering...");
        // 1) 从日志文件中读取出一条条日志记录中国的data域, 从而求出最大页号
        lg.rewind(); // 定位到日志文件的
        int maxPgno = 0;
        byte[] log_data = null;
        while((log_data = lg.next()) != null) { // next方法返回的是日志记录的data域
            int pgno;
            if(isInsertLog(log_data)) { // 如果是insert类型
                InsertLogInfo li = parseInsertLog(log_data);
                pgno = li.pgno;
            } else {
                UpdateLogInfo li = parseUpdateLog(log_data);
                pgno = li.pgno;
            }
            // 求maxPgno
            if(pgno > maxPgno) {
                maxPgno = pgno;
            }
        }
        if(maxPgno == 0) {
            maxPgno = 1;
        }
        //
        // 2) 通过PageCache把db文件中maxPgno页面后面的页全部删除(截断):
        // 因为此时求出的maxPgno是日志里面求出来的最大页号, 因为页号是递增的, 所以小于maxPgno的页面必然都在日志里出现过了
        // 而根据我们之前设定的WAL机制, 那么如果不在日志里记录过的页(即:比maxPgno大的页), 必然是不应该再存在的
        // 所以将其后面的全部截断
        pc.truncateByBgno(maxPgno);
        System.out.println("Truncate to " + maxPgno + " pages.");

        // 3) 开始redo
        redoTransactions(tm, lg, pc);
        System.out.println("Redo Transactions Over.");
        // 4) 开始undo
        undoTransactions(tm, lg, pc);
        System.out.println("Undo Transactions Over.");

        System.out.println("Recovery Over.");
    }

    // 重做redo所有崩溃时已完成（committed 或 aborted）的事务
    // **正序**扫描事务 T 的所有日志
    // 如果日志是插入操作 (Ti, I, A, x)，就将 x 重新插入 A 位置
    // 如果日志是更新操作 (Ti, U, A, oldx, newx)，就将 A 位置的值设置为 newx
    // 注意: 本项目其实没有真正的删除操作，对于插入操作的 undo，只是将其中的标志位设置为 invalid。对于删除的探讨将在 VM 一节中进行。
    private static void redoTransactions(TransactionManager tm, Logger lg, PageCache pc) {
        // 先定位到日志文件的第一条日志记录开始处
        lg.rewind();
        // 1) 从日志文件中一行行读取出log的data域,进行处理
        byte[] log_data = null;
        while ((log_data = lg.next()) != null) {
            // 2) 判断类型, 对其进行封装, 并通过tm判断其是否是已完成（committed 或 aborted）的事务,如果是执行redo操作
            if(isInsertLog(log_data)) {
                InsertLogInfo insertLogInfo = parseInsertLog(log_data);
                long xid = insertLogInfo.xid;
                if(!tm.isActive(xid)) {
                    doInsertLog(pc, insertLogInfo, REDO);
                }
            } else {
                UpdateLogInfo updateLogInfo = parseUpdateLog(log_data);
                long xid = updateLogInfo.xid;
                if(!tm.isActive(xid)) {
                    doUpdateLog(pc, updateLogInfo, REDO);
                }
            }

        }
    }

    // 撤销undo所有崩溃时未完成（active）的事务
    // **倒序**扫描事务 T 的所有日志
    // 如果日志是插入操作 (Ti, I, A, x)，就将 A 位置的数据删除
    // 如果日志是更新操作 (Ti, U, A, oldx, newx)，就将 A 位置的值设置为 oldx
    private static void undoTransactions(TransactionManager tm, Logger lg, PageCache pc) {
        // 此时注意:
        // 因为是逆序的, 所以此时我们要用一个顺序容器List将同一个事务的所有日志给记录下来, 然后最后对每一个事务的所有日志来逆序遍历
        // 很明显, 应该用一个Map<XID,日志List>数据结构
        Map<Long, List<byte[]>> logMap = new HashMap<>();
        // 先定位到日志文件的第一条日志记录开始处
        lg.rewind();
        // 1) 从日志文件中一行行读取出log的data域,进行处理
        byte[] log_data = null;
        while ((log_data = lg.next()) != null) {
            // 2) 判断类型, 对其进行封装, 并通过tm判断其是否是未完成(active)的事务,如果是则存入对应xid的List中
            if(isInsertLog(log_data)) {
                InsertLogInfo insertLogInfo = parseInsertLog(log_data);
                long xid = insertLogInfo.xid;
                if(tm.isActive(xid)) {
                    if(!logMap.containsKey(xid)) { // 如果之前还没有出现过该事务,则新建一个List
                        logMap.put(xid,new ArrayList<>());
                    }
                    logMap.get(xid).add(log_data);
                }
            } else {
                UpdateLogInfo updateLogInfo = parseUpdateLog(log_data);
                long xid = updateLogInfo.xid;
                if(tm.isActive(xid)) {
                    if(!logMap.containsKey(xid)) { // 如果之前还没有出现过该事务,则新建一个List
                        logMap.put(xid,new ArrayList<>());
                    }
                    logMap.get(xid).add(log_data);
                }
            }
        }

        // 3) 对所有active log进行倒序undo操作
        for(Map.Entry<Long, List<byte[]>> entry : logMap.entrySet()) {
            List<byte[]> logs = entry.getValue(); // 获得该xid事务对应的所有日志
            // 逆序遍历:
            for (int i = logs.size() - 1; i >= 0; i --) {
                byte[] log = logs.get(i);
                if(isInsertLog(log)) {
                    doInsertLog(pc, parseInsertLog(log), UNDO);
                } else {
                    doUpdateLog(pc, parseUpdateLog(log), UNDO);
                }
            }
            // 对一个异常关闭前的active事务undo结束后, 该事务就应该置为: abort状态, 也就是: 已撤销(回滚)
            tm.abort(entry.getKey());
        }
    }


    /**
     * 执行插入日志记录相关的恢复操作
     * @param pc 页面缓存机制(可以操作db文件)
     * @param insertLogInfo 插入日志记录对应的对象
     * @param type 类型: redo / undo
     */
    private static void doInsertLog(PageCache pc, InsertLogInfo insertLogInfo, int type) {
        // 1) 先获取到对应页面
        Page page = null;
        try {
            page = pc.getPage(insertLogInfo.pgno);
        } catch (Exception e) {
            Panic.panic(e);
        }
        // 2) 根据之前的规定: 本项目其实没有真正的删除操作，对于插入操作的 undo，只是将其中的标志位设置为 invalid。对于删除的探讨将在 VM 一节中进行。
        //    所以无论是redo还是undo, 都要往page里面插入数据
        //    只不过如果是undo, 要进行标记
        try {
            // 如果是undo, 要将其中的标志位设置为 invalid。对于删除的探讨将在 VM 一节中进行。
            if(type == UNDO) {
                DataItem.setDataItemRawInvalid(insertLogInfo.raw);
            }
            // 调用PageXDataHandler的recoverInsert方法, 实现往页面里的offset位置进行插入
            PageXDataHandler.recoverInsert(page, insertLogInfo.raw, insertLogInfo.offset);
        } finally {
            // 因为getPage会让引用+1, 所以此时要取消引用
            page.release();
        }
    }
    /**
     * 执行更新日志记录相关的恢复操作
     * @param pc 页面缓存机制(可以操作db文件)
     * @param updateLogInfo 更新日志记录对应的对象
     * @param type 类型: redo / undo
     */
    private static void doUpdateLog(PageCache pc, UpdateLogInfo updateLogInfo, int type) {
        // 1) 先获取到对应页面
        Page page = null;
        try {
            page = pc.getPage(updateLogInfo.pgno);
        } catch (Exception e) {
            Panic.panic(e);
        }
        try {
            // 2) 根据之前的规定:
            // 如果是redo, 则把页面offset位置更新为新值
            if(type == REDO) {
                // 如果是
                PageXDataHandler.recoverUpdate(page,updateLogInfo.newRaw,updateLogInfo.offset);
            }
            // 如果是undo, 则把页面offset位置重置为旧值
            else {
                PageXDataHandler.recoverUpdate(page,updateLogInfo.oldRaw,updateLogInfo.offset);
            }
        } finally {
            // 因为getPage会让引用+1, 所以此时要取消引用
            pc.release(page);
        }
    }

    private static UpdateLogInfo parseUpdateLog(byte[] log) {
        UpdateLogInfo updateLogInfo = new UpdateLogInfo();
        // 根据data的格式和之前设置的偏移量, 一一从中取出数值进行封装
        updateLogInfo.xid = Parser.parseLong(Arrays.copyOfRange(log,OF_XID,OF_PGNO));
        updateLogInfo.pgno = Parser.parseInt(Arrays.copyOfRange(log,OF_PGNO,OF_OFFSET));
        updateLogInfo.offset = Parser.parseShort(Arrays.copyOfRange(log,OF_OFFSET,OF_RAW));
        // 对于update而言, 因为一个是旧值一个是新值, 所以两者的类型必然是一样的, 所以长度必然是一样的, 所以对半分
        int raw_length = (log.length - OF_RAW) / 2;
        updateLogInfo.oldRaw = Arrays.copyOfRange(log,OF_RAW,log.length + raw_length);
        updateLogInfo.newRaw = Arrays.copyOfRange(log,OF_RAW + raw_length,log.length + 2*raw_length);
        return updateLogInfo;
    }

    private static InsertLogInfo parseInsertLog(byte[] log) {
        InsertLogInfo insertLogInfo = new InsertLogInfo();
        // 根据data的格式和之前设置的偏移量, 一一从中取出数值进行封装
        insertLogInfo.xid = Parser.parseLong(Arrays.copyOfRange(log,OF_XID,OF_PGNO));
        insertLogInfo.pgno = Parser.parseInt(Arrays.copyOfRange(log,OF_PGNO,OF_OFFSET));
        insertLogInfo.offset = Parser.parseShort(Arrays.copyOfRange(log,OF_OFFSET,OF_RAW));
        insertLogInfo.raw = Arrays.copyOfRange(log,OF_RAW,log.length);
        return insertLogInfo;
    }

    private static boolean isInsertLog(byte[] log) {
        return log[0] == LOG_TYPE_INSERT;
    }

    // 生成一条updateLog: [LogType][XID][Pgno][Offset][OldRaw][NewRaw]
    public static byte[] updateLog(long xid, DataItem di) {
        byte[] logType = {LOG_TYPE_UPDATE};
        byte[] xidRaw = Parser.long2Byte(xid);
        byte[] pgno = Parser.int2Byte(di.getPgno());
        byte[] offset = Parser.short2Byte(di.getOffset());
        byte[] oldRaw = di.getOldRaw();
        SubArray raw = di.getRaw();
        byte[] newRaw = Arrays.copyOfRange(raw.raw, raw.start, raw.end);
        return Bytes.concat(logType, xidRaw, pgno, offset, oldRaw, newRaw);
    }

    // 生成一条insertLog: [LogType][XID][Pgno][Offset][Raw]
    public static byte[] insertLog(long xid, Page pg, byte[] raw) {
        byte[] logTypeRaw = {LOG_TYPE_INSERT};
        byte[] xidRaw = Parser.long2Byte(xid);
        byte[] pgno = Parser.int2Byte(pg.getPageNumber());
        byte[] offset = Parser.short2Byte(PageXDataHandler.getFSO(pg));
        return Bytes.concat(logTypeRaw, xidRaw, pgno, offset, raw);
    }
}
